iT邦幫忙

2023 iThome 鐵人賽

DAY 17
0
Mobile Development

SwiftUI 的大大小小系列 第 17

Day 17 - 在 SwiftUI 使用 @ViewBuilder

  • 分享至 

  • xImage
  •  

hero

前言

在做固定格式的 UI 時,例如有一個 title ,內容會有不同形式的內容,例如:

  • 表單欄位
    • 表單 label 樣式固定,表單內容可能是不同種類的 textfields
  • 聊天室的訊息
    • 名稱和頭像固定,內容可以有照片、影片、文字 etc.

這時候就可以把相同的東西封裝起來,透過 @ViewBuilder 把需要不同的區塊讓外部以參數的方式傳入。

初始案例

落落長的做了個 UI 像是這樣

1701

除了內容物以外的 UI 元件都是重複的,先看程式碼:

程式碼

struct ContentView: View {
    var body: some View {
        ScrollView {
            VStack(alignment: .leading) {
                HStack(alignment: .top) {
                    Circle()
                        .foregroundColor(.blue)
                        .frame(width: 30, height: 30)
                        .offset(y: 8)
                    VStack(alignment: .leading, spacing: 8) {
                        Text("金色浪黃斥皮走北中樹錯牙犬羊院兩走夕助:完住收己氣辛。少像同乞國從方青京室怎童行假花,她雞食蝶蝸問怕何反大松,吧右丟奶麻方掃。停了平坡勿沒科小媽辛裝小種書嗎風、戊陽很用急游。")
                            .font(.system(size: 14))
                            .padding()
                            .background(
                                RoundedRectangle(cornerRadius: 5)
                                    .foregroundStyle(Color(red: 0.92, green: 0.92, blue: 0.95))
                            )
                        Text("已讀")
                            .font(.system(size: 10))
                            .foregroundStyle(.gray)
                            .padding(.horizontal, 8)
                    }
                }
                .padding([.horizontal, .bottom], 8)
                HStack(alignment: .top) {
                    Circle()
                        .foregroundColor(.blue)
                        .frame(width: 30, height: 30)
                        .offset(y: 8)
                    VStack(alignment: .leading, spacing: 8) {
                        Image(systemName: "fan.desk")
                            .resizable()
                            .foregroundStyle(.indigo)
                            .rotation3DEffect(
                                .degrees(-20),
                                axis: (x: 0.0, y: 1.0, z: 0.0)
                            )
                            .aspectRatio(contentMode: .fit)
                            .frame(width: 100, height: 100)
                            .padding()
                            .background(
                                RoundedRectangle(cornerRadius: 5)
                                    .foregroundStyle(Color(red: 0.92, green: 0.92, blue: 0.95))
                            )
                        Text("已讀")
                            .font(.system(size: 10))
                            .foregroundStyle(.gray)
                            .padding(.horizontal, 8)
                    }
                }
                .padding([.horizontal, .bottom], 8)
            }
        }
    }
}

拆解和封裝

可以發現無論文字還是圖片,重複的地方就是這一塊

HStack(alignment: .top) {
    Circle()
        .foregroundColor(.blue)
        .frame(width: 30, height: 30)
        .offset(y: 8)
    VStack(alignment: .leading, spacing: 8) {
            /** 內容物 **/
            .padding()
            .background(
                RoundedRectangle(cornerRadius: 5)
                    .foregroundStyle(Color(red: 0.92, green: 0.92, blue: 0.95))
            )
        Text("已讀")
            .font(.system(size: 10))
            .foregroundStyle(.gray)
            .padding(.horizontal, 8)
    }
}
.padding([.horizontal, .bottom], 8)

於是可以先把他封裝起來,這邊用了 content 作為需要顯示的內容,型別則透過泛行指定為 Content ,而 Content 則必須為 View ,如下

struct Message<Content>: View where Content : View {
    var content: Content
    var body: some View {
        HStack(alignment: .top) {
            Circle()
                .foregroundColor(.blue)
                .frame(width: 30, height: 30)
                .offset(y: 8)
            VStack(alignment: .leading, spacing: 8) {
                    content
                    .padding()
                    .background(
                        RoundedRectangle(cornerRadius: 5)
                            .foregroundStyle(Color(red: 0.92, green: 0.92, blue: 0.95))
                    )
                Text("已讀")
                    .font(.system(size: 10))
                    .foregroundStyle(.gray)
                    .padding(.horizontal, 8)
            }
        }
        .padding([.horizontal, .bottom], 8)
    }
}

使用

替換掉後會像是這樣

struct ContentView: View {
    var body: some View {
        ScrollView {
            VStack(alignment: .leading) {
                Message(content: Text("金色浪黃斥皮走北中樹錯牙犬羊院兩走夕助:完住收己氣辛。少像同乞國從方青京室怎童行假花,她雞食蝶蝸問怕何反大松,吧右丟奶麻方掃。停了平坡勿沒科小媽辛裝小種書嗎風、戊陽很用急游。")
                    .font(.system(size: 14))
                )
                Message(content: Image(systemName: "fan.desk")
                    .resizable()
                    .foregroundStyle(.indigo)
                    .rotation3DEffect(
                        .degrees(-20),
                        axis: (x: 0.0, y: 1.0, z: 0.0)
                    )
                    .aspectRatio(contentMode: .fit)
                    .frame(width: 100, height: 100)
                )
            }
        }
    }
}

但是為了更接近 SwiftUI 原生的寫法,應該要用 closure 的方式傳入,而不是像這樣用的方式傳。

這時候就要用到 @ViewBuilder

自定義 init 方法,套用 @ViewBuilder

幫上面的 Message 加上一個 init 方法:

  • 將 content 宣告成 closure 型別
  • 將這個參數指定為 @ViewBuilder
init(@ViewBuilder content: () -> Content) {
    self.content = content()
}

最後程式碼

Message 就能夠和一般的 SwiftUI 的元件一樣,以 closure 的方式傳入我們期望要顯示的畫面:

struct ContentView: View {
    var body: some View {
        ScrollView {
            VStack(alignment: .leading) {
                Message {
                    Text("金色浪黃斥皮走北中樹錯牙犬羊院兩走夕助:完住收己氣辛。少像同乞國從方青京室怎童行假花,她雞食蝶蝸問怕何反大松,吧右丟奶麻方掃。停了平坡勿沒科小媽辛裝小種書嗎風、戊陽很用急游。")
                        .font(.system(size: 14))
                }
                Message {
                    Image(systemName: "fan.desk")
                        .resizable()
                        .foregroundStyle(.indigo)
                        .rotation3DEffect(
                            .degrees(-20),
                            axis: (x: 0.0, y: 1.0, z: 0.0)
                        )
                        .aspectRatio(contentMode: .fit)
                        .frame(width: 100, height: 100)
                }
            }
        }
    }
}

最後的 Message

struct Message<Content>: View where Content : View {
    var content: Content
    
    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }
    
    var body: some View {
        HStack(alignment: .top) {
            Circle()
                .foregroundColor(.blue)
                .frame(width: 30, height: 30)
                .offset(y: 8)
            VStack(alignment: .leading, spacing: 8) {
                    content
                    .padding()
                    .background(
                        RoundedRectangle(cornerRadius: 5)
                            .foregroundStyle(Color(red: 0.92, green: 0.92, blue: 0.95))
                    )
                Text("已讀")
                    .font(.system(size: 10))
                    .foregroundStyle(.gray)
                    .padding(.horizontal, 8)
            }
        }
        .padding([.horizontal, .bottom], 8)
    }
}

1701

結語

那今天的 SwiftUI 的大大小小就到這邊,以上,明天見!

環境

  • Xcode 15 beta 8

本篇使用到的 UI 元件和 modifiers 基本上沒有受到版本更新影響

因此 Xcode 14 等環境下使用也是沒問題的。


上一篇
Day 16 - 在 SwiftUI 實作基本的 NavigationStack
下一篇
Day 18 - 在 SwiftUI 使用 @ViewBuilder 之 2
系列文
SwiftUI 的大大小小30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言